Skip to content

Preserve large-integer precision when decoding tool arguments - #528

Closed
PratikDhanave (PratikDhanave) wants to merge 6 commits into
microsoft:mainfrom
PratikDhanaveFork:fix-jsonformat-int-precision
Closed

Preserve large-integer precision when decoding tool arguments#528
PratikDhanave (PratikDhanave) wants to merge 6 commits into
microsoft:mainfrom
PratikDhanaveFork:fix-jsonformat-int-precision

Conversation

@PratikDhanave

Copy link
Copy Markdown
Contributor

Summary

applySchema (agent/format/jsonformat/encoding.go), reached via Format.Unmarshal, decodes incoming JSON arguments into an interface{} with json.Unmarshal, which turns every JSON number into a float64. Integer arguments beyond 2^53 are therefore silently truncated before being re-marshalled and handed to the typed handler:

// handler takes struct{ N int64 `json:"n"` }
tool.Call(ctx, `{"n":9007199254740993}`)   // 2^53 + 1
// handler receives N == 9007199254740992  (off by one) — no error

Every functool call with an int64/uint64 argument is affected. (The output / Normalize path is not — it operates on the already-typed value and doesn't round-trip through this number-losing step.)

Fix

Decode with json.Decoder + UseNumber(), so numbers are preserved as json.Number and round-trip exactly through the re-marshal.

Public API

No exported symbols change.

Tests

Adds TestFormat_Unmarshal_PreservesLargeIntegerPrecision (black-box): a 2^53+1 argument. It comes back as 9007199254740992 before this change and exactly 9007199254740993 after. Full ./agent/format/jsonformat and ./tool/functool suites pass (no regression).

@PratikDhanave
PratikDhanave (PratikDhanave) requested a review from a team as a code owner July 17, 2026 18:48
Copilot AI review requested due to automatic review settings July 17, 2026 18:48

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes loss of integer precision (> 2^53) when decoding tool-call JSON arguments by switching the schema-application step to decode numbers as json.Number instead of float64, ensuring large int64/uint64 arguments round-trip correctly into typed handlers.

Changes:

  • Update applySchema to decode JSON with json.Decoder + UseNumber() to preserve large integer precision.
  • Add a black-box regression test proving Format.Unmarshal preserves a 2^53+1 integer.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
agent/format/jsonformat/encoding.go Switch argument decoding to UseNumber() in the schema/defaults path to avoid float64 truncation.
agent/format/jsonformat/encoding_test.go Add regression test ensuring Unmarshal preserves large int64 values.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread agent/format/jsonformat/encoding.go
Comment on lines 5 to 9
import (
"bytes"
"encoding/json"
"fmt"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — added the io import and a strict trailing-data check (dec.Token() must return io.EOF). Note: validate() can't also use UseNumber — json.Number is a string type the jsonschema validator rejects as a non-integer, which broke every test; left as a comment in the PR.

Comment on lines +73 to +77
// Decode with UseNumber so integers beyond 2^53 are not silently
// truncated by being decoded into float64 and re-marshalled.
dec := json.NewDecoder(bytes.NewReader(data))
dec.UseNumber()
if err := dec.Decode(&v); err != nil {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The UseNumber decode preserves large-integer precision in the result value the tool receives. validate() re-decodes separately only for schema checking (pre-existing behavior) — json.Number is a string kind the jsonschema validator rejects as non-integer, so it can't be used there. The precision fix applies where it matters (the decoded result) without altering validation.

@PratikDhanave
PratikDhanave (PratikDhanave) force-pushed the fix-jsonformat-int-precision branch 2 times, most recently from 22af497 to f7805b6 Compare July 23, 2026 15:44
@github-actions

This comment has been minimized.

@github-actions github-actions Bot added the parity-approved Go API consistency review found no parity issues label Jul 23, 2026
@github-actions

This comment has been minimized.

applySchema decoded incoming JSON arguments into an interface{} via
json.Unmarshal, which turns every JSON number into a float64. Integer
arguments beyond 2^53 (int64/uint64) were therefore silently truncated
before being re-marshalled and handed to the typed handler — e.g. a tool
called with {"n":9007199254740993} received 9007199254740992.

Decode with json.Decoder.UseNumber() so numbers are preserved as json.Number
and round-trip exactly. Only the input path (Format.Unmarshal) was affected;
the output/Normalize path already operates on the typed value.

Adds a black-box test asserting a 2^53+1 argument round-trips exactly.
Address review feedback: switching to json.Decoder for UseNumber made
applySchema tolerate trailing data after the first JSON value, unlike the
json.Unmarshal it replaced. Restore strict single-value parsing by
requiring io.EOF after the decoded value, and add a regression test.

(The related suggestion to also decode with UseNumber inside validate()
is not applied: json.Number is a string type that the jsonschema
validator rejects as a non-integer, so it cannot be used there.)
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions github-actions Bot added area:agent Changes files in the agent area size:medium At most 100 changed lines across at most 5 files pending-auto-risk Automatic risk classification is in progress labels Aug 20, 2026
@github-actions

This comment has been minimized.

@github-actions github-actions Bot added failed-auto-risk Automatic risk classification was inconclusive or failed and removed pending-auto-risk Automatic risk classification is in progress labels Aug 20, 2026
@github-actions github-actions Bot added pending-auto-risk Automatic risk classification is in progress and removed failed-auto-risk Automatic risk classification was inconclusive or failed labels Aug 22, 2026
@github-actions

This comment has been minimized.

@github-actions github-actions Bot added risk:medium Contained production impact requiring normal review depth and removed pending-auto-risk Automatic risk classification is in progress labels Aug 22, 2026
@github-actions github-actions Bot added pending-auto-risk Automatic risk classification is in progress risk:medium Contained production impact requiring normal review depth and removed risk:medium Contained production impact requiring normal review depth pending-auto-risk Automatic risk classification is in progress labels Aug 26, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Scope: internal-only (unexported implementation detail with user-visible behavioral effect)
Changed Go contract: None — no exported symbols changed. The fix changes applySchema (unexported) in agent/format/jsonformat/encoding.go to use json.Decoder + UseNumber() instead of json.Unmarshal, preserving int64/uint64 precision through the tool argument round-trip.
Upstream evidence reviewed:

  • python/packages/core/agent_framework/_tools.py — Python FunctionTool.invoke uses Pydantic model_validate, which parses integer JSON fields natively without float64 truncation.
  • .NET System.Text.Json (used by the .NET tools layer) also deserialises integers to their target CLR type without a float64 intermediate — no equivalent precision bug exists there.
    Result: ✅ parity-approved — the fix closes a Go-specific regression against the correct behavior already present in the Python and .NET implementations. No new exported API surface is introduced; the public-api-change label is not warranted.

Generated by Go API Consistency Review Agent · sonnet46 · 19.9 AIC · ⌖ 5 AIC · ⊞ 6.4K ·

@qmuntal

Copy link
Copy Markdown
Member

Thanks for investigating this and adding a clear reproduction. The precision issue is real, but we don’t want to resolve it with UseNumber().

This changes every decoded numeric value in the exported Result any field from float64 to json.Number, which is not what users would expect. Limiting the change to large values would instead make the concrete type depend on the number’s magnitude, which is also undesirable.

Addressing this safely requires a broader serialization design that preserves raw JSON without changing the established decoded representation. We’re therefore closing this PR rather than merging the current approach.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:agent Changes files in the agent area parity-approved Go API consistency review found no parity issues risk:medium Contained production impact requiring normal review depth size:medium At most 100 changed lines across at most 5 files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants